package icu.funkye.confine.limiter;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import icu.funkye.confine.common.concurrent.ConfineThreadFactory;
import icu.funkye.confine.common.loader.EnhancedServiceLoader;
import icu.funkye.confine.limiter.product.AbstractLimiter;
import icu.funkye.confine.limiter.product.Limiter;
import icu.funkye.confine.wrapper.ResourceWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static icu.funkye.confine.limiter.RateLimiterFactory.LimiterType.BUCKET_TYPE;
import static icu.funkye.confine.wrapper.ResourceWrapper.LimitMode;
import static icu.funkye.confine.wrapper.ResourceWrapper.LimitMode.DROP;
import static icu.funkye.confine.wrapper.ResourceWrapper.LimitMode.WAIT;

/**
 * @author jianbin.chen
 */
public class RateLimiterFactory {

    private static final Map<String, ResourceWrapper> rateCache = new ConcurrentHashMap<>();

    private static final ScheduledThreadPoolExecutor EXECUTOR =
        new ScheduledThreadPoolExecutor(1, new ConfineThreadFactory("computeRt"));

    private static final Logger LOGGER = LoggerFactory.getLogger(RateLimiterFactory.class);

    private static volatile LimiterType TYPE = BUCKET_TYPE;

    public static void init(LimiterType limitType) {
        TYPE = limitType;
    }

    static {
        EXECUTOR.scheduleAtFixedRate(() -> rateCache.forEach((k, v) -> {
            try {
                List<Long> rtList = v.getRtList();
                int size = rtList.size();
                if (size > 0) {
                    Long sumTotal = 0L;
                    for (Long rt : rtList) {
                        sumTotal += rt;
                    }
                    BigDecimal sum = new BigDecimal(sumTotal);
                    BigDecimal rtSize = new BigDecimal(size);
                    v.setAverageRt(sum.divide(rtSize, RoundingMode.HALF_UP).longValue());
                    rtList.clear();
                } else {
                    v.setAverageRt(0L);
                }
            } catch (Exception e) {
                LOGGER.error(e.getMessage(), e);
            }
        }), 0, 60, TimeUnit.SECONDS);
    }

    public static ResourceWrapper create(String method, int tps, int waitTime, LimitMode limitType) {
        return create(method, tps, waitTime, limitType, 0L, false, true);
    }

    public static ResourceWrapper create(String method, int tps, int waiTime) {
        return create(method, tps, waiTime, WAIT);
    }

    public static ResourceWrapper create(String method, int tps) {
        return create(method, tps, 0, DROP);
    }

    public static ResourceWrapper create(String method) {
        return create(method, 50);
    }

    public static ResourceWrapper create(String method, int tps, int waitTime, LimitMode limitType, Boolean enable) {
        return create(method, tps, waitTime, limitType, 0L, false, enable);
    }

    public static ResourceWrapper create(String method, int tps, int waiTime, Boolean enable) {
        return create(method, tps, waiTime, WAIT, enable);
    }

    public static ResourceWrapper create(String method, int tps, Boolean enable) {
        return create(method, tps, 0, DROP, enable);
    }

    public static ResourceWrapper create(String method, int tps, int waitTime, Long maxRt, LimitMode limitType) {
        return create(method, tps, waitTime, limitType, maxRt, false, true);
    }

    public static ResourceWrapper create(String method, int tps, int waiTime, Long maxRt) {
        return create(method, tps, waiTime, maxRt, WAIT);
    }

    public static ResourceWrapper create(String method, int tps, Long maxRt) {
        return create(method, tps, 0, maxRt, DROP);
    }

    public static ResourceWrapper create(String method, Long maxRt) {
        return create(method, 50, maxRt);
    }

    public static ResourceWrapper create(String method, int tps, int waiTime, boolean cover) {
        return create(method, tps, waiTime, WAIT, 0L, cover, true);
    }

    public static ResourceWrapper create(String method, int tps, boolean cover) {
        return create(method, tps, 0, DROP, 0L, cover, true);
    }

    public static ResourceWrapper create(String method, boolean cover) {
        return create(method, 50, cover);
    }

    public static ResourceWrapper create(String method, int tps, int waitTime, LimitMode limitType, Long maxRt,
        boolean cover, Boolean enable) {
        ResourceWrapper resourceWrapper;
        if (!cover) {
            resourceWrapper = rateCache.get(method);
            if (resourceWrapper == null) {
                resourceWrapper = rateCache.computeIfAbsent(method, k -> new ResourceWrapper(method, limitType));
            }
            if (resourceWrapper.getRateLimiter() == null) {
                synchronized (resourceWrapper) {
                    if (resourceWrapper.getRateLimiter() == null) {
                        resourceWrapper.setRateLimiter(limiterFactory(TYPE).create(method, tps)).setEnable(enable);
                        resourceWrapper.setWaitTime(waitTime);
                        resourceWrapper.setMaxRt(maxRt);
                    }
                }
            }
        } else {
            resourceWrapper = new ResourceWrapper(method, limitType);
            resourceWrapper.setRateLimiter(limiterFactory(TYPE).create(method, tps));
            resourceWrapper.setWaitTime(waitTime);
            rateCache.put(method, resourceWrapper);
        }
        return resourceWrapper;
    }

    public static ResourceWrapper get(String method) {
        return rateCache.get(method);
    }

    public static void update(String method, int maxTps, long maxRt, boolean enable) {
        ResourceWrapper resourceWrapper = create(method);
        Limiter limiter = resourceWrapper.getRateLimiter();
        limiter.setEnable(enable);
        if (maxTps >= 0) {
            limiter.setTps(maxTps);
        }
        if (maxRt >= 0) {
            resourceWrapper.setMaxRt(maxRt);
        }
    }

    public static void update(String method, long maxRt, boolean enable) {
        update(method, -1, maxRt, enable);
    }

    public static void update(String method, long maxRt) {
        update(method, maxRt, true);
    }

    public static void update(String method, int maxTps) {
        update(method, maxTps, true);
    }

    public static void update(String method, int maxTps, boolean enable) {
        update(method, maxTps, -1, enable);
    }

    private static Limiter limiterFactory(LimiterType type) {
        return EnhancedServiceLoader.load(AbstractLimiter.class, type.getType());
    }

    public enum LimiterType {

        GUAVA_TYPE("guava"), BUCKET_TYPE("bucket");

        String type;

        LimiterType(String type) {
            this.type = type;
        }

        public String getType() {
            return type;
        }

        public static LimiterType get(String type) {
            if (GUAVA_TYPE.getType().equalsIgnoreCase(type)) {
                return GUAVA_TYPE;
            }
            return BUCKET_TYPE;
        }
    }

}
